package com.nulldev.util.scripting.ScriptAPI.baseJS.legacy.lexer;

import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.List;
import java.util.regex.MatchResult;
import java.util.regex.Pattern;

import com.nulldev.util.JVM.Arguments;
import com.nulldev.util.JVM.Exceptions;
import com.nulldev.util.JVM.JVM;
import com.nulldev.util.VariableAPI.ArrayUtils;
import com.nulldev.util.VariableAPI.StringsUtil;
import com.nulldev.util.VariableAPI.util.strings.splitters.Splitters;
import com.nulldev.util.concurrency.threadIt.v4.emapi.ExecutorManager;
import com.nulldev.util.concurrency.threadIt.v4.emapi.uft.Executable;
import com.nulldev.util.data.Variables;
import com.nulldev.util.data.variable;
import com.nulldev.util.logging.Logger;
import com.nulldev.util.logging.LoggerFactory;
import com.nulldev.util.scripting.ScriptAPI.baseJS.legacy.lexer.evaler.BaseJSEvaluator;
import com.nulldev.util.scripting.ScriptAPI.baseJS.legacy.lexer.global.BaseJSConstants;
import com.nulldev.util.scripting.ScriptAPI.baseJS.legacy.lexer.global.BaseJSNativeFunction;

public class BaseJSInterp implements BaseJSConstants {

	/*
	 * BaseJS Layout: eval()/interpret() \-> Evaluator (pre-process/executor [link
	 * to Interpreter] - only via interpret()) \ BaseJSEnvironment \-> Interpreter
	 * (executor)
	 */

	public static final Logger log = LoggerFactory.getLoggerD(true);

	public BaseJSEnvironment interpret(final String code) {
		return interpret(false, code);
	}

	public BaseJSEnvironment interpret(final boolean useNewInterpreter, final String code) {
		if (code == null || code.isEmpty())
			throw new NullPointerException("code == null!");
		final BaseJSEnvironment e = new BaseJSEnvironment(useNewInterpreter);
		if (useNewInterpreter) {
			BaseJSEvaluator.eval(e, cleanInvalidCharacters(code));
			return e;
		} else {
			final String[] lines = cleanInvalidCharacters(code).split("};");
			for (final String line : lines) {
				final Object r = this.lineInterp(e, line);
				if (r != UNDEFINED)
					e.setReturnValue(r);
			}
		}
		return e;
	}

	public BaseJSEnvironment interpret(final boolean useNewInterpreter, final String code, final BaseJSEnvironment e) {
		if (code == null || code.isEmpty())
			throw new NullPointerException("code == null!");
		Variables.requireNonNull(e, "e");
		if (useNewInterpreter) {
			BaseJSEvaluator.eval(e, cleanInvalidCharacters(code));
			return e;
		} else {
			final String[] lines = cleanInvalidCharacters(code).split("};");
			for (final String line : lines) {
				final Object r = this.lineInterp(e, line);
				if (r != UNDEFINED)
					e.setReturnValue(r);
			}
		}
		return e;
	}

	public Executable<BaseJSEnvironment> interpretAsTask(final boolean useNewInterpreter, final String code) {
		final BaseJSEnvironment e = new BaseJSEnvironment(useNewInterpreter);
		return ExecutorManager.make(() -> {
			if (useNewInterpreter) {
				BaseJSEvaluator.eval(e, cleanInvalidCharacters(code));
				return e;
			} else {
				final String[] lines = cleanInvalidCharacters(code).split("};");
				for (final String line : lines) {
					final Object r = this.lineInterp(e, line);
					if (r != UNDEFINED)
						e.setReturnValue(r);
				}
			}
			return e;
		});
	}

	protected void interpOperation(String line, final BaseJSFunction function, final BaseJSEnvironment e, final Object... variables) {
		if (line == null)
			throw new NullPointerException("line == null!");
		if (line.isEmpty())
			return;
		while (line.startsWith(" "))
			line = line.substring(1);
		log.trace("interpOperation() -> line: " + line + ", function: " + function + ", e: " + e + ", args: " + variables + ", cache: " + function.valueCache);
		if (line.startsWith("var")) {
			final String variable = StringsUtil.substringBetweenO(line, "var ", "=");
			final String value = line.substring(line.indexOf('=') + 1);
			final BaseJSVariable v = new BaseJSVariable(variable);
			function.valueCache.put(variable, v);
			if (value.contains("[") && value.contains("]")) {
				final String pre = value.substring(0, value.indexOf('['));
				final String interValue = StringsUtil.substringBetweenO(value, "[", "]");
				if (pre == null || pre.isEmpty()) {
					final String[] values = interValue.split(",");
					final Object[] array = new Object[values.length];
					copyArguments(values, array);
					v.set(array);
				} else {
					if (!function.valueCache.containsKey(pre))
						throw new MissingFunctionError("Missing variable: " + pre);
					BaseJSVariable f = function.valueCache.get(pre);
					if (f.isArray()) {
						v.set(this.processArrayEntry(interValue, e, f, function));
						log.trace("isArray() -> " + interValue + " -> " + v);
					} else if (f.isVariable()) {
						f = (BaseJSVariable) f.get();
						v.set(this.processArrayEntry(interValue, e, f, function));
						log.trace("BUG_HIDDEN_VARS -> " + interValue + " -> " + v);
					} else
						throw new UnexpectedObjectTypeError("Variable '" + pre + "' should be an array! [cacheValue: " + f + ", line: " + line + "]");
				}
			} else {
				v.set(StringsUtil.isNumber(value) ? StringsUtil.asNumber(value) : value);
			}
		} else {
			if (line.contains("=")) {
				String variable = line.substring(0, line.indexOf('='));
				final String value = line.substring(line.indexOf('=') + 1);
				log.trace(line + " -> " + variable + " -> " + value);
				String _object_loc = null;
				log.trace(line + " -> variable: " + variable + ", value: " + value);
				if (variable.contains("[") && variable.endsWith("]")) {
					_object_loc = StringsUtil.substringBetweenO(variable, "[", "]");
					variable = variable.substring(0, variable.indexOf('['));
				}
				BaseJSVariable o = null;
				if (function.valueCache.containsKey(variable)) {
					o = function.valueCache.get(variable);
				} else {
					o = new BaseJSVariable(variable, StringsUtil.isNumber(variable) ? StringsUtil.asNumber(variable) : value);
					function.valueCache.put(variable, o);
				}
				if (o != null && o.isVariable())
					o = (BaseJSVariable) o.get();
				Object result = null;
				if (value.contains("[") && value.endsWith("]")) {
					final String s = StringsUtil.substringBetweenO(value, "[", "]");
					result = this.processArrayEntry(s, e, o, function);
				} else if (function.valueCache.containsKey(value)) {
					result = function.valueCache.get(value).get();
				} else if (value.contains(".")) {
					final String pre = value.substring(0, value.indexOf('.'));
					final String op = value.substring(value.indexOf('.') + 1);
					result = this.processObject(op, value, e, (function.hasObject(pre, e) ? function.getObject(pre, e) : o), function);
					log.trace("processObject -> op: " + op + ", result: " + Arrays.toString((String[]) result));
				} else if (StringsUtil.isNumber(value)) {
					result = StringsUtil.asNumber(value);
				} else {
					log.warn("Unknown result gen: " + value);
				}
				log.trace("_object_loc: " + _object_loc + ", isArray: " + o.isArray() + ", o: " + o + ", var: " + variable);
				if (_object_loc != null && o.isArray()) {
					if (!StringsUtil.isNumber(_object_loc)) {
						_object_loc = String.valueOf(this.processArrayEntry(_object_loc, e, o, function));
					}
					if (StringsUtil.isNumber(value)) {
						Array.set(o.get(), (int) StringsUtil.asNumber(_object_loc), result);
					} else {
						Object _value;
						if (result instanceof Integer) {
							_value = Array.get(o.get(), ((int) result));
						} else if (result instanceof Double) {
							_value = Array.get(o.get(), (int) ((double) result));
						} else {
							_value = result;
						}
						Array.set(o.get(), StringsUtil.asIntegerNumber(_object_loc), _value);
					}
				} else {
					log.trace("set: " + result);
					o.set(result);
				}
			} else if (line.startsWith("return ")) {
				final String[] v = line.split(" ");
				final String a = v[1];
				if (a.contains(".")) {
					final String[] r = a.split("\\.");
					final String var = r[0];
					final String t = r[1];
					if (function.valueCache.containsKey(var)) {
						function.returnValue.lazySet(this.processObject(t, a, e, function.valueCache.get(var), function));
						;
					} else if (e.contents.containsKey(var) && e.contents.get(var).isVariable()) {
						function.returnValue.lazySet(this.processObject(t, a, e, (BaseJSVariable) e.contents.get(var), function));
						;
					} else if (e.contents.containsKey(var)) {
						final BaseJSObject o = e.contents.get(var);
						final String _t = t.substring(0, t.indexOf('('));
						final String tv = StringsUtil.substringBetweenO(t, "(", ")");
						if (!o.contains(_t))
							throw new MissingFunctionError("Missing item: " + t);
						final BaseJSObject f = o.get(_t);
						if (tv != null && !tv.isEmpty()) {
							final String[] args = tv.split(",");
							final Object[] vals = new Object[args.length];
							copyArguments(args, vals, function);
							if (f.isNativeFunction())
								function.returnValue.lazySet(((BaseJSNativeFunction) f).run(e, vals));
							else
								throw new UnexpectedObjectTypeError("Cannot handle: " + t);
						} else {
							if (f.isNativeFunction())
								function.returnValue.lazySet(((BaseJSNativeFunction) f).run(e));
							else
								throw new UnexpectedObjectTypeError("Cannot handle: " + t);
						}
					} else
						throw new MissingFunctionError("Missing variable: " + var);
				} else if (a.contains("+") || a.contains("/") || a.contains("-")) {
					function.returnValue.lazySet(this.evalStatement(a, e, function));
				} else if (a.contains("[") && a.contains("]")) {
					final String var = a.substring(0, a.indexOf('['));
					final String val = StringsUtil.substringBetweenO(a, "[", "]");
					if (!function.valueCache.containsKey(var))
						throw new MissingFunctionError("Missing variable: " + var);
					final BaseJSVariable fs = function.valueCache.get(var);
					if (!fs.isArray())
						throw new UnexpectedObjectTypeError(var + " is not an array!");
					final int index = Integer.parseInt(val);
					function.returnValue.lazySet(Array.get(fs.value, index));
				} else {
					if (function.valueCache.containsKey(a)) {
						function.returnValue.lazySet(function.valueCache.get(a).get());
					} else
						throw new MissingFunctionError("Missing variable: " + a);
				}
			} else if (line.contains(".")) {
				final String variable = line.substring(0, line.indexOf('.'));
				final String value = line.substring(line.indexOf('.') + 1);
				if (!function.hasObject(variable, e)) {
					// FIXME
					log.debug("line: " + line);
					throw new MissingFunctionError("Missing variable: " + variable);
				}
				final BaseJSVariable v = function.getObject(variable, e);
				v.set(this.processObject(value, line, e, v, function));
			} else if (line.contains("[") && line.contains("](") && line.endsWith(")")) {
				final String variable = line.substring(0, line.indexOf(']') + 1);
				final String obj = variable.substring(0, variable.indexOf("["));
				if (!function.hasObject(obj, e)) {
					// FIXME
					log.debug("line: " + line);
					throw new MissingFunctionError("Missing variable: " + obj);
				}
				final BaseJSObject v = e.contents.get(obj);
				final String f = variable.substring(variable.indexOf("[") + 1, variable.indexOf("]"));
				if (v == null || !v.contains(f)) {
					throw new MissingFunctionError("Missing variable: " + f);
				}
				final BaseJSObject _v = v.get(f);
				final String value = line.substring(line.indexOf('(') + 1, line.length() - 2);
				if (_v.isFunction()) {
					final String[] args = value.split(",");
					final Object[] vals = new Object[args.length];
					copyArguments(args, vals, function);
					((BaseJSFunction) _v).run(e, vals);
				} else if (_v.isVariable())
					this.processObject(value, line, e, (BaseJSVariable) _v, function);
			} else {
				log.warn("Unknown operation: " + line);
			}
		}
	}

	private static final String MATH_OPERATORS_REGEX = "\\+|\\*|\\/|\\-||\\%";

	private Object evalStatement(final String line, final BaseJSEnvironment e, final BaseJSFunction function) {
		if (line.contains("+") || line.contains("*") || line.contains("/") || line.contains("-") || line.contains("%")) {
			log.trace(line);
			final List<String> items = ArrayUtils.toArrayListFast(line.split(MATH_OPERATORS_REGEX));
			final Iterable<MatchResult> v = StringsUtil.allMatches(Pattern.compile(MATH_OPERATORS_REGEX), line);
			double buffer = 0;
			double varBuffer = 0;
			String lastOperator = "";
			for (final MatchResult match : v) {
				final String _m = match.group();
				String m = _m;
				if (m.isEmpty() && !items.isEmpty()) {
					final String item = items.remove(0);
					if (StringsUtil.isNumber(item)) {
						varBuffer = (int) StringsUtil.asNumber(item);
					} else if (function != null && function.valueCache.containsKey(item)) {
						final BaseJSVariable xv = function.valueCache.get(item);
						xv.nullPtrCheck();
						if (xv.isNumber()) {
							varBuffer = (xv.get() instanceof Double ? ((double) xv.get()) : ((int) xv.get()));
						} else {
							varBuffer = (double) StringsUtil.asNumber((String) xv.get());
						}
					} else
						throw new UnexpectedObjectTypeError("Unknown item: " + item);
				} else {
					if (!items.isEmpty())
						items.remove(0);
					if (m.isEmpty())
						m = lastOperator;
					// FIXME: Comply to BODMAS/BIDMAS/whatever the fuck you want.
					log.trace("buffer: " + buffer + ", varBuffer: " + varBuffer + ", operator: " + m + ", raw: " + _m);
					switch (m) {
						case "+": {
							log.trace(buffer + " + " + varBuffer + " = " + (buffer + varBuffer));
							buffer += varBuffer;
							break;
						}
						case "*": {
							log.trace(buffer + " * " + varBuffer + " = " + (buffer * varBuffer));
							buffer *= varBuffer;
							break;
						}
						case "-": {
							buffer -= varBuffer;
							break;
						}
						case "%": {
							buffer %= varBuffer;
							break;
						}
						case "/": {
							if (varBuffer == 0)
								throw new IllegalArgumentException("Cannot divide by 0!");
							buffer /= varBuffer;
							break;
						}
						default:
							throw new UnexpectedObjectTypeError("Unknown operator: " + m);
					}
					lastOperator = m;
				}
			}
			return buffer;
		}
		throw new UnexpectedObjectTypeError("Cannot handle: " + line);
	}

	private Object processObject(final String line, final String fullLine, final BaseJSEnvironment e, BaseJSVariable object, final BaseJSFunction function) {
		if (object == null)
			throw new NullPointerException("object == null!");
		log.trace("processObject(): line=" + line + ", cache: " + function.valueCache);
		if (object.isVariable()) {
			object = (BaseJSVariable) object.get();
		}
		if (object.isArray() && line.equals("length"))
			return object.length();
		if (object.isString() && line.equals("length"))
			return object.length();
		if (object.isString() && line.equals("reverse()"))
			return StringsUtil.reverse((String) object.get());
		if (object.isArray() && line.equals("reverse()")) {
			if (object.get() instanceof String[]) {
				final String[] c = (String[]) object.get();
				ArrayUtils.reverse(c);
				object.set(c);
				return c;
			} else if (object.get() instanceof int[]) {
				final int[] c = (int[]) object.get();
				ArrayUtils.reverse(c);
				object.set(c);
				return c;
			} else if (object.get() instanceof Object[]) {
				final Object[] c = (Object[]) object.get();
				ArrayUtils.reverse(c);
				object.set(c);
				return c;
			}
			log.warn("reverse(N/A): line: " + line + ", items: " + Arrays.toString((Object[]) object.get()));
			return null;
		}
		if (object.isString() && line.startsWith("split(") && line.endsWith(")")) {
			final String arg = StringsUtil.substringBetween(line, "split(", ")");
			if (arg.equalsIgnoreCase("\"\""))
				return ((String) object.get()).split("");
			return ((String) object.get()).split(arg);
		}
		if (object.isArray() && line.startsWith("join(") && line.endsWith(")")) {
			final String arg = StringsUtil.substringBetween(line, "join(", ")");
			if (arg.equalsIgnoreCase("\"\"") && object.get() instanceof String[])
				return StringsUtil.join((String[]) object.get(), "");
			if (object.get() instanceof String[])
				return StringsUtil.join((String[]) object.get(), arg);
		}
		if (object.isArray() && line.startsWith("splice(") && line.endsWith(")")) {
			final String[] args = Splitters.splitString(StringsUtil.substringBetween(line, "splice(", ")"), ",");
			final int[] d = new int[args.length];
			for (int i = 0; i < d.length; i++) {
				if (StringsUtil.isNumber(args[i])) {
					d[i] = (int) StringsUtil.asNumber(args[i]);
				} else if (function.valueCache.containsKey(args[i])) {
					d[i] = function.valueCache.get(args[i]).tryInt();
				} else {
					throw new UnexpectedObjectTypeError("Unknown object: " + args[i]);
				}
			}
			if (d.length < 2 || d.length > 2)
				throw new UnexpectedLengthError("splice() takes 2 arguments.");
			log.trace("splice() -> from: " + d[0] + ", to: " + d[1] + ", line: " + line);
			if (object.get() instanceof int[]) {
				final int[] f = Arrays.copyOfRange((int[]) object.get(), d[1], ((int[]) object.get()).length);
				object.set(f);
				return f;
			} else {
				final Object[] f = Arrays.copyOfRange((Object[]) object.get(), d[1], ((Object[]) object.get()).length);
				object.set(f);
				return f;
			}
		}
		if (object.isObject()) {
			final String func = line.substring(0, line.indexOf('(')).trim();
			BaseJSObject o = (BaseJSObject) object.get();
			if (o.contents.containsKey(func) && o.contents.get(func).isFunction()) {
				final String[] args = StringsUtil.substringBetween(line, func + "(", ")").split(",");
				final Object[] d = new Object[args.length];
				copyArguments(args, d, function);
				if (o.contents.get(func).isNativeFunction()) {
					return ((BaseJSNativeFunction) o.contents.get(func)).run(e, d);
				} else
					return ((BaseJSFunction) o.contents.get(func)).run(e, d);

			}
		}
		throw new MissingFunctionError("Unknown function: " + line);
	}

	private Object processArrayEntry(final String line, final BaseJSEnvironment e, BaseJSVariable array, final BaseJSFunction function) {
		if (array.isVariable()) {
			array = (BaseJSVariable) array.get();
		}
		if (array == null)
			throw new NullPointerException("array == null!");
		if (!array.isArray())
			throw new UnexpectedObjectTypeError("array isn't a array! (wat)");
		if (line == null)
			throw new NullPointerException("line == null!");
		if (StringsUtil.isNumber(line)) {
			return Array.get(array.get(), Integer.parseInt(line));
		} else if (line.contains("%")) {
			final String[] ops = line.trim().split("%");
			Object[] data = new Object[ops.length];
			for (int i = 0; i < ops.length; i++) {
				final String op = ops[i];
				if (op.contains(".")) {
					final String name = op.substring(0, op.indexOf('.'));
					if (!function.valueCache.containsKey(name))
						throw new MissingVariableError("Cannot find variable: " + name);
					final String sub = op.substring(op.indexOf('.') + 1);
					data[i] = processObject(sub, line, e, function.valueCache.get(name), function);
				} else {
					if (function.valueCache.containsKey(op)) {
						data[i] = function.valueCache.get(op).get();
					} else
						throw new MissingVariableError("Cannot find variable: " + op);
				}
			}
			double v = (int) data[0];
			for (int i = 1; i < data.length; i++) {
				v = (v % ((int) data[i]));
			}
			log.trace("% -> data: " + Arrays.toString(data) + ", v: " + v + ", line: " + line);
			return v;
		} else {
			throw new UnexpectedObjectTypeError("processArrayEntry -> Cannot process: " + line);
		}
	}

	private static void copyArguments(final String[] args, final Object[] output) {
		copyArguments(args, output, null);
	}

	private static enum InternalArgCopyAction {
		ITERATE,
		BREAK,
		CONTINUE
	}

	private static InternalArgCopyAction internalArgCopy(String arg, final int i, final variable<Boolean> IS_ENCLOSED_ARRAY, final String[] args,
			final Object[] output, final BaseJSFunction function) {
		log.trace("internalArgCopy() -> arg: " + arg + ", i: " + i + ", IS_ENCLOSED_ARRAY: " + IS_ENCLOSED_ARRAY);
		while (arg.startsWith(" "))
			arg = arg.substring(1);
		if (arg.isEmpty())
			return InternalArgCopyAction.CONTINUE;
		if (StringsUtil.isNumber(arg)) {
			output[i] = StringsUtil.asNumber(arg);
			return InternalArgCopyAction.ITERATE;
		} else if (StringsUtil.isString(arg)) {
			output[i] = StringsUtil.substringBetweenO(arg, "\"", "\"");
			return InternalArgCopyAction.ITERATE;
		} else if (arg.equals("true")) {
			output[i] = true;
			return InternalArgCopyAction.ITERATE;
		} else if (arg.equals("false")) {
			output[i] = false;
			return InternalArgCopyAction.ITERATE;
		} else if (function != null && function.valueCache.containsKey(arg)) {
			output[i] = function.valueCache.get(arg);
			return InternalArgCopyAction.ITERATE;
		} else if (ArrayUtils.first(args).startsWith("[") && ArrayUtils.last(args).endsWith("]")) {
			IS_ENCLOSED_ARRAY.set(true);
			return InternalArgCopyAction.BREAK;
		} else {
			throw new UnexpectedObjectTypeError("Unknown object: " + args[i]);
		}
	}

	/*
	 * FIXME: Enclosed arrays still keep their original values after [0].
	 */
	private static void copyArguments(final String[] args, final Object[] output, final BaseJSFunction function) {
		final variable<Boolean> IS_ENCLOSED_ARRAY = new variable<Boolean>(false);
		for (int i = 0; i < output.length; i++) {
			String arg = args[i];
			log.trace("copy() -> arg: " + arg);
			final InternalArgCopyAction o = internalArgCopy(arg, i, IS_ENCLOSED_ARRAY, args, output, function);
			if (o == InternalArgCopyAction.BREAK)
				break;
			else if (o == InternalArgCopyAction.CONTINUE)
				continue;
		}
		if (IS_ENCLOSED_ARRAY.get()) {
			final Object[] arr = new Object[args.length];
			for (int i = 0; i < output.length; i++) {
				String arg = args[i];
				if (arg.startsWith("["))
					arg = arg.substring(1);
				if (arg.endsWith("]"))
					arg = arg.substring(0, arg.length() - 1);
				final InternalArgCopyAction o = internalArgCopy(arg, i, IS_ENCLOSED_ARRAY, args, output, function);
				if (o == InternalArgCopyAction.BREAK)
					throw new UnknownOperationError("copyArguments@IS_ENCLOSED_ARRAY: Cannot escape an already enclosed array!");
				else if (o == InternalArgCopyAction.CONTINUE)
					continue;
			}
			output[0] = arr;
		}
	}

	private Object lineInterp(final BaseJSEnvironment e, final String line) {
//		BaseJSEvaluator.eval(line);
		if (line.startsWith("var")) {
			// DEFINE
			final String variable = StringsUtil.substringBetweenO(line, "var ", "=");
			final BaseJSObject o = new BaseJSObject(variable);
			String value = line.substring(line.indexOf('=') + 1);
			if (value.startsWith("{") && value.endsWith("}")) { // we an object
				value = value.substring(1, value.length() - 1);
				final String[] values = value.split("},");
				for (String object : values) {
					final String name = object.substring(0, object.indexOf(':'));
					String payload = object.substring(object.indexOf(':') + 1, object.length());
					if (payload.startsWith("function("))
						payload += "}"; // fix lost }
					if (payload.startsWith("function(")) {
						final String[] a = StringsUtil.substringBetweenO(payload, "(", "){").split(",");
						final String[] operations = StringsUtil.substringBetweenO(payload, "){", "}").split(";");
						final BaseJSFunction func = new BaseJSFunction(name, a, operations);
						o.contents.put(name.trim(), func);
					} else {
						log.warn("unknown: " + payload);
					}
				}
			} else if (value.contains("(") && (value.endsWith(");") || value.endsWith(")"))) { // we a function
				final String function = value.substring(0, value.indexOf('('));
				if (!e.contents.containsKey(function))
					throw new MissingFunctionError("Function not found: " + function);
				final BaseJSObject ix = e.contents.get(function);
				final String[] args = StringsUtil.substringBetween(value, "(", ")").split(",");
				final Object[] d = new Object[args.length];
				copyArguments(args, d);
				if (ix.isNativeFunction()) {
					return ((BaseJSNativeFunction) ix).run(e, d);
				} else if (ix.isFunction()) {
					return ((BaseJSFunction) ix).run(e, d);
				} else
					throw new UnexpectedObjectTypeError("Cannot execute: " + ix);
			}
			e.contents.put(variable.trim(), o);
		} else {
			if (line.contains("=")) {
				final String name = line.substring(0, line.indexOf('='));
				String content = line.substring(line.indexOf('=') + 1);
				log.trace("[lineInterp:contains(=)] " + name + " = " + content);
				if (content.startsWith("function(")) {
					content += "}";
					final String[] a = StringsUtil.substringBetweenO(content, "(", "){").split(",");
					final String[] operations = StringsUtil.substringBetweenO(content, "){", "}").split(";");
					final BaseJSFunction f = new BaseJSFunction(name, a, operations);
					e.contents.put(name.trim(), f);
				} else {
					log.debug("[lineInterp:contains(=)] Unknown line: " + line);
				}
			} else if (line.contains("(") && (line.endsWith(");") || line.endsWith(")"))) {
				final String function = line.substring(0, line.indexOf('('));
				BaseJSObject obj = null;
				if (function.contains(".")) {
					final String[] sp = function.split("\\.");
					if (sp.length == 2) {
						obj = e.contents.get(sp[0]);
						obj = obj.get(sp[1]);
					} else {
						throw new UnknownOperationError("TODO: Support recursive query.");
					}
				} else {
					obj = e.contents.getOrDefault(function, null);
				}
				if (obj == null)
					throw new MissingFunctionError("Function not found: " + function);
//				if (!e.contents.containsKey(function)) throw new MissingFunctionError("Function not found: " + function);
				final BaseJSObject ix = obj; // e.contents.get(function);
				final String[] args = StringsUtil.substringBetween(line, "(", ")").split(",");
				final Object[] d = new Object[args.length];
				copyArguments(args, d);
				if (ix.isNativeFunction()) {
					return ((BaseJSNativeFunction) ix).run(e, d);
				} else if (ix.isFunction()) {
					return ((BaseJSFunction) ix).run(e, d);
				} else
					throw new UnexpectedObjectTypeError("Cannot execute: " + ix);
			} else if (line.startsWith("return ")) {
				final String function = line.substring("return ".length(), (line.endsWith(";") ? line.length() - 1 : line.length()));
				if (function.equals("true"))
					return true;
				else if (function.equals("false"))
					return false;
				else
					throw new UnexpectedObjectTypeError("Cannot return: " + function);
			} else if (line.contains("-") || line.contains("+") || line.contains("%")) {
				evalStatement(line, e, null);
			} else {
				log.debug("[lineInterp::parse] Unknown line: " + line);
			}
		}
		return UNDEFINED;
	}

	private static String cleanInvalidCharacters(String in) {
		final StringBuilder out = new StringBuilder();
		char current;
		if (in == null || ("".equals(in))) {
			return "";
		}
		for (int i = 0; i < in.length(); i++) {
			current = in.charAt(i);
			if ((current == 0x9) || (current == 0xA) || (current == 0xD) || ((current >= 0x20) && (current <= 0xD7FF))
					|| ((current >= 0xE000) && (current <= 0xFFFD)) || ((current >= 0x10000) && (current <= 0x10FFFF))) {
				out.append(current);
			}

		}
		return out.toString().replaceAll("\\s", " ");
	}

	public static void main(String[] args) throws Exception {
		final boolean USE_NEW_INTERP = true;
		Arguments.setup(args);
		{
			// test-ntube-v8
			final BaseJSEnvironment f = new BaseJSInterp().interpret(USE_NEW_INTERP, "var Ew={yJ:function(a,b){a.splice(0,b)},"
					+ "QX:function(a,b){var c=a[0];a[0]=a[b%a.length];a[b%a.length]=c},"
					+ "AX:function(a){a.reverse()}};_NG=function(a){a=a.split(\"\");Ew.AX(a,52);Ew.yJ(a,3);Ew.QX(a,1);Ew.QX(a,67);Ew.yJ(a,2);Ew.QX(a,8);return a.join(\"\")};");
//			ArrayUtils.printLln(f.contents);
			// AOq0QJ8wRgIhAMnPWGLB5wSHkhieIABHg12r3DwPIEkLhWVxyemQkrr-AiEApXwyZUyz6Rg06vMhcrJnrA4IKrNYQ8oOOBx10EV0dec=
			// <- expected
			final Object r = ((BaseJSFunction) f.get("_NG")).run(USE_NEW_INTERP, f,
					"=ced0VE01xBOOo8QYNrKI4ArnJrchMv60gR6zyRZywXpAEiA-rrkQmeyxVWhLkEIPwD3r21gHBAIeihkHSw5BLGWPnMAhIgAw8JQ0qORUORUO");
			JVM.println("v8-ntube-result: " + r);
			if (r instanceof String && ((String) r)
					.equalsIgnoreCase("AOq0QJ8wRgIhAMnPWGLB5wSHkhieIABHg12r3DwPIEkLhWVxyemQkrr-AiEApXwyZUyz6Rg06vMhcrJnrA4IKrNYQ8oOOBx10EV0dec=")) {
				JVM.println("test-ntube-v8: PASSED");
			} else {
				JVM.println("test-ntube-v8: FAILED");
			}
		}
		{
			try {
				// test-ntube-v9
				// Required things to implement: ARRAY_PARSING, INLINE_FUNCTIONS, FOR_LOOPS,
				// SWITCH_CASES, BREAK_AND_CONTINUE, FOR_EACH, MATH_COMPLIANT
				final BaseJSEnvironment f = new BaseJSInterp().interpret(USE_NEW_INTERP,
						"nea=function(a){a=a.split(\"\");var b=[572290758,-1690221916,572290758,1380389349,-1915579074,function(c,d){d=(d%c.length+c.length)%c.length;c.splice(-d).reverse().forEach(function(e){c.unshift(e)})},"
								+ "820582474,function(c,d){d=(d%c.length+c.length)%c.length;var e=c[0];c[0]=c[d];c[d]=e},"
								+ "1786399956,469317042,-1915579074,766621134,function(c,d){for(d=(d%c.length+c.length)%c.length;d--;)c.unshift(c.pop())},"
								+ "-596276970,305066270,null,-300502647,a,1533196389,function(c,d){for(var e=64,f=[];++e-f.length-32;){switch(e){case 58:e-=14;case 91:case 92:case 93:continue;case 123:e=47;case 94:case 95:case 96:continue;case 46:e=95}f.push(String.fromCharCode(e))}c.forEach(function(h,l,m){this.push(m[l]=f[(f.indexOf(h)-f.indexOf(this[l])+l-32+e--)%f.length])},d.split(\"\"))},"
								+ "-925405942,1676110244,1583854628,-1374831156,509354977,function(c){c.reverse()},"
								+ "1200533416,-1985728292,-2146932091,-596276970,-1583091306,function(c){for(var d=c.length;d;)c.push(c.splice(--d,1)[0])},"
								+ "-1045570850,1238960618,null,function(c,d){d=(d%c.length+c.length)%c.length;c.splice(0,1,c.splice(d,1,c[0])[0])},"
								+ "function(c,d){d=(d%c.length+c.length)%c.length;c.splice(d,1)},"
								+ "-1568600387,a,584992847,-683934414,1744191347,1090904194,1892882931,\"nW6LP15\",a,null];b[15]=b;b[34]=b;b[46]=b;b[7](b[46],b[30]);b[36](b[45],b[29]);b[36](b[17],b[8]);b[7](b[45],b[26]);b[31](b[17]);b[0](b[34],b[1]);b[27](b[14],b[3]);b[45](b[8],b[3]);b[1](b[4],b[27]);b[44](b[30]);b[32](b[11],b[10]);b[44](b[28]);b[28](b[16],b[6]);b[45](b[18],b[24]);b[44](b[16],b[15]);b[28](b[42],b[38]);b[44](b[35],b[22]);b[28](b[35],b[31]);b[26](b[0],b[17]);b[44](b[35],b[43]);b[2](b[16]);b[28](b[34],b[46]);b[36](b[5],"
								+ "b[0]);b[12](b[41]);b[28](b[20]);b[8](b[1],b[44]);b[34](b[20]);b[28](b[1]);b[38](b[18],b[35]);b[38](b[1],b[6]);b[3](b[5],b[23]);b[44](b[14],b[3]);b[22](b[43]);b[42](b[0],b[27]);b[14](b[30],b[25]);b[0](b[34],b[28]);b[6](b[34],b[25]);b[42](b[45],b[5]);b[10](b[40],b[19]);b[26](b[42]);return a.join(\"\")};");
				ArrayUtils.printLln(f.contents);
				final Object r = ((BaseJSFunction) f.get("nea")).run(USE_NEW_INTERP, f,
						"=ced0VE01xBOOo8QYNrKI4ArnJrchMv60gR6zyRZywXpAEiA-rrkQmeyxVWhLkEIPwD3r21gHBAIeihkHSw5BLGWPnMAhIgAw8JQ0qORUORUO");
				JVM.println("v9-ntube-result: " + r);
				if (r instanceof String && ((String) r)
						.equalsIgnoreCase("MkJiGfaSOoSQCx2gcgBGZAVEnA6veNd9HwBb0uJP3mxMz_JKL9d1ZFHJR0fZpGkzlgLLvd8pIxFpYvhA7C3aKcaRtDaW1gikW0aiJegM")) {
					JVM.println("test-ntube-v9: PASSED");
				} else {
					JVM.println("test-ntube-v9: FAILED");
				}
			} catch (Exception e) {
				JVM.println("test-ntube-v9: FAILED");
				JVM.println(Exceptions.toString(e));
			}
		}
		{
			// test variables
			final BaseJSEnvironment f = instance.interpret(USE_NEW_INTERP, "n=function(){var a=1;b=2;c=3;return a+b*c*5;}");
			// Should be 31 but B[??]MAS isn't a thing yet.
			final Object r = ((BaseJSFunction) f.get("n")).run(USE_NEW_INTERP, f);
			JVM.println("test-chain-math: " + (((double) r) == 31 ? "PASSED" : (((double) r) == 30 ? "FAILED" : "FAILED")) + " [result: " + r + "]");
		}
		{
			// Math.random()
			final BaseJSEnvironment f = instance.interpret(USE_NEW_INTERP, "n=function(){return Math.random();}");
			final Object r = ((BaseJSFunction) f.get("n")).run(USE_NEW_INTERP, f);
			JVM.println("test-math-random: " + r);
		}
		{
			// isArray-var
			final BaseJSEnvironment f = instance.interpret(USE_NEW_INTERP, "n=function(){var f=[1,2,3];return Array.isArray(f);}");
			final Object r = ((BaseJSFunction) f.get("n")).run(USE_NEW_INTERP, f);
			JVM.println("isArray-var: " + r);
		}
		{
			// isArray-enclosed
			final BaseJSEnvironment f = instance.interpret(USE_NEW_INTERP, "n=function(){return Array.isArray([1,2,3]);}");
			final Object r = ((BaseJSFunction) f.get("n")).run(USE_NEW_INTERP, f);
			JVM.println("isArray-enclosed: " + r);
		}
		{
			// array-read
			final BaseJSEnvironment f = instance.interpret(USE_NEW_INTERP, "n=function(){var f=[1,2,3];return f[2];}");
			final Object r = ((BaseJSFunction) f.get("n")).run(USE_NEW_INTERP, f);
			JVM.println("array-read: " + r);
		}
		{
			// array-redef-read
			final BaseJSEnvironment f = instance.interpret(USE_NEW_INTERP, "n=function(){var f=[1,2,3];f[2]=5;return f[2];}");
			final Object r = ((BaseJSFunction) f.get("n")).run(USE_NEW_INTERP, f);
			JVM.println("array-redef-read: " + r);
		}
		{
			// console-log
			final BaseJSEnvironment f = instance.interpret(USE_NEW_INTERP,
					"n=function(){console.log(\"Hello world from base.js!\", \"Now with multiple entries!\");}");
			final Object r = ((BaseJSFunction) f.get("n")).run(USE_NEW_INTERP, f);
			JVM.println("console-log: " + (r == null ? "PASSED" : "FAILED"));
		}
		{
			// parser-val-eval
			final BaseJSEnvironment f = instance.interpret(USE_NEW_INTERP, "n=function(){var f=[1,2,3];f[2]=5;return f[2];};var c=n();");
			if (f.hasReturnValue()) {
				JVM.println("parser-val-eval: PASSED (return value: " + f.getReturnValue() + ")");
			} else if (!f.contents.containsKey("c") || !f.contents.get("c").isVariable()) {
				JVM.println("parser-val-eval: FAILED");
				JVM.println("\t· c wasn't found!");
			} else {
				JVM.println("parser-val-eval: " + f + " (pass)");
			}
		}
		{
			// parser-eval
			final BaseJSEnvironment f = instance.interpret(USE_NEW_INTERP, "n=function(){var f=[1,2,3];f[2]=5;return f[2];};n();");
			if (f.hasReturnValue()) {
				JVM.println("parser-eval: PASSED (return value: " + f.getReturnValue() + ")");
			} else if (!f.contents.containsKey("c") || !f.contents.get("c").isVariable()) {
				JVM.println("parser-eval: FAILED");
			} else {
				JVM.println("parser-eval: " + f + " (pass)");
			}
		}
		{
			// parser-format-spacing via array-redef-read
			final BaseJSEnvironment f = instance.interpret(USE_NEW_INTERP, "n=function(){var f=[1,2,3]; f[2]=5; return f[2];}");
			final Object r = ((BaseJSFunction) f.get("n")).run(USE_NEW_INTERP, f);
			JVM.println("parser-format-spacing: " + (String.valueOf(r).contentEquals("5") ? "PASSED" : "FAILED"));
		}
		{
			try {
				// parser-double-reverse -> aka no u
				final BaseJSEnvironment f = instance.interpret(USE_NEW_INTERP, "n=function(){var f=[1,2,3]; return f.reverse().reverse();}");
				final Object r = ((BaseJSFunction) f.get("n")).run(USE_NEW_INTERP, f);
				JVM.println("parser-double-reverse: "
						+ (String.valueOf(r).contentEquals("[1, 2, 3]") ? "PASSED" : "FAILED [got: " + Arrays.toString((Object[]) r) + "]"));
			} catch (Exception e) {
				JVM.errorln(e);
			}
		}
		{
			// values-booleans
			final BaseJSEnvironment f = instance.interpret(USE_NEW_INTERP, "return true;");
			if (f.hasReturnValue()) {
				JVM.println("values-booleans: " + (String.valueOf(f.getReturnValue()).contentEquals("true") ? "PASSED" : "FAILED"));
			} else {
				JVM.println("values-booleans: FAILED");
			}
		}
		{
			// eval-booleans
			final BaseJSEnvironment f = instance.interpret(USE_NEW_INTERP, "eval(\"return true\");");
			if (f.hasReturnValue()) {
				JVM.println("eval-booleans: " + (String.valueOf(f.getReturnValue()).contentEquals("true") ? "PASSED" : "FAILED"));
			} else {
				JVM.println("eval-booleans: FAILED");
			}
		}
	}

	static final BaseJSInterp instance = new BaseJSInterp();

	public static BaseJSInterp get() {
		return instance;
	}
}
